<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!--
This file requires a JSONMessage service running under Java on the local host.

TODO: document how to do that (at least with Glassfish)
-->
 <head>
  <title>JSONMessage Demo</title>
<!--
Prototype's JSON support is apparently hosed: as soon as i include Prototype, JSON-able objects
containing empty arrays or empty objects get mis-quoted, causing conversion errors downstream.
e.g. [] gets JSON'd as "[]"! This happens even though i'm not using Prototype for anything other
than the actual AJAX transmission.

<script src='http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js' type='text/javascript'></script>

Same thing with mootools!!!

<script src='http://ajax.googleapis.com/ajax/libs/mootools/1.2.4/mootools.js' type='text/javascript'></script>

Bug reporg:
https://prototype.lighthouseapp.com/projects/8886/tickets/1039-jsonstringify-incorrectly-jsonizes-empty-arrays-when-prototype-is-imported

status: fixed in prototype 1.7. As of this writing (20100421), 1.7 isn't yet released via googleapis.

Dojo appears to work fine for this purpose, however:

<script src='http://ajax.googleapis.com/ajax/libs/dojo/1.4.1/dojo/dojo.xd.js' type='text/javascript'></script>

As does jQuery (of course). Tested with 1.2.6+. Might work with older versions.
-->
<style type='text/css'>
.message-timestamp {
    background-color:lightgray;
    color:darkslateblue;
    font-size:80%;
/*    font-family: monospace;*/
}
.error-message
{
    color:red;
}

/*ul.bogomenu-menu
{
    background-color: #fafafa;
    border: 1px inset black;
}*/

span.bogomenu-branch
{
    font-size: 1em;
    /*font-style: italic;*/
    font-weight: bold;
    color: darkgreen;
}
hr.bogomenu-separator
{
    border-top: 1px solid black;
    border-bottom: none;
}
span.bogomenu-branch:hover
{
    color: white;
    background-color: darkblue;
    /*border: 2px outset black;*/
}
span.bogomenu-leaf
{
    font-size: 0.8em;
    font-style: italic;
    /*font-family: monospace;*/
}
span.bogomenu-leaf:hover
{
    color: white;
    background-color: darkgreen;
    /*border: 2px outset black;*/
}
#ajaxNotification {
/* to place it at the top/left of the browser window,
move the element into the BODY somewhere and then add
something like:

  position:absolute;
  top:0;
  left:0;
  width:300px;
  border: 1px inset darkblue;
*/
  background-color:#0099ff;
  color:white;
  opacity:0.75;

}
</style>
<!-- note that the css/js are ordered as they are because Chrome told me to do it this way. -->
<link rel="stylesheet" href='demo.css' type="text/css"/>
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js' type='text/javascript'></script>
<script src='lib/jquery.html.js' type='text/javascript'>/*needed by jquery.bogomenu.js*/</script>
<script src='lib/jquery.bogomenu.js' type='text/javascript'>/*needed by this demo, but not by JSONMessage*/</script>
<script src='lib/JSONMessage/extendClass.js' type='text/javascript'>/* subclassing mechanism framework */</script>
<script src='lib/JSONMessage/JSONable.inc.js' type='text/javascript'>/* JSONable base class framework */</script>
<script src='lib/JSONMessage/JSONMessage.inc.js' type='text/javascript'>/* JSONMessage framework */</script>
<script type='text/javascript'>


/** A print()-like messaging function. All arguments
    are PREpended to jQuery('#logArea'), along with a
    timestamp.
*/
function message()
{
    var av = Array.prototype.slice.apply(arguments,[0]);
    var ts = jQuery('<span>').addClass('message-timestamp').
        text( (new Date()).toLocaleString()+":" );
    av.unshift( ts );
    var l = jQuery('<div>');
    var k;
    for( k = 0; k < av.length; ++k )
    {
        l.append( av[k] );
        if( k < (av.length-1) ) l.append(' ');
    }
    jQuery('#logArea').prepend(l);
}


function createObjectInspector(from)
{
    var code = from ? from : jQuery('#textResponse').attr('value');
    if( ! code )
    {
        message("Enter a JSON message object before continuing!");
        return;
    }
    var req = null;
    try
    {
        req = new JSONMessage.Request(code);
    }
    catch(e)
    {
        alert("Creation of JSONMessage.Request failed while parsing JSON:\n"+e);
        return;
    }

    src = req.toJSONable();
    function insert( key, val )
    {
        var type = typeof val;
        var suffix = '';
        var ent = {};
        var k, v, cutoff = 32/*max visible length of values*/;
        if( JSONMessage.isArray( val ) )
        {
            ent.entries = [];
            for( k in val )
            {
                ent.entries.push( arguments.callee( k, val[k] ) );
            }
            suffix = '(length='+ent.entries.length+')';
        }
        else if( 'object' === typeof val )
        {
            ent.entries = [];
            var c = 0;
            for( k in val )
            {
                ent.entries.push( arguments.callee( k, val[k] ) );
                ++c;
            }
            suffix = '('+c+' props.)';
        }
        else if( 'function' === typeof val )
        {
        }
        else
        {
            v = JSON.stringify(val);
            if( v.length > cutoff )
            {
                v = v.substring(0,cutoff) +'...';
                ent.onclick = function(menuElement,jqElement)
                {
                    alert(key+"=\n"+val);
                };
            }
            suffix = '= '+v;
        }
        ent.title = type+' '+key+' '+suffix;
        return ent;
    }
    var menu = insert( 'root', src );
    var view = jQuery.createBogoMenu(menu);
    jQuery('#inspectorTest').empty().append(view);
}

function updateResponseView()
{
    var src = jQuery('#textResponse');
    var code = src.attr('value');
    try
    {
        createObjectInspector(JSON.parse(code));
    }
    catch(e)
    {
        alert("Exception: "+e);
    }
}

/**
    Sends a ChuckNorris event and displays the result
    in an alert() dialog.
*/
function chuckNorris()
{
    jQuery('#inspectorTest').empty();
    var m = new JSONMessage.Request();
    m.type('ChuckNorris');
    jQuery('#textRequest').attr('value',m.toJSON());
    m.post( {
        onResponse:function(resp) {
            if( 0 == resp.resultCode() )
            {
                var p = resp.payload();
                var msg = "Chuck Norris quote #"+p.number+" of "+p.of+":\n\n"
                    +p.quote
                jQuery('#textResponse').attr('value',resp.toJSON());
                updateResponseView();
                alert( msg );
            }
        },
        onError:function(request,opt)
        {
            alert("Chuck fucking failed:\n"+
                    JSON.stringify(opt,undefined,'  ')
                    );
        }
    });
}
function sendBrokenMessage()
{
    var m = new JSONMessage.Request();
    m.type('ChuckNorris');
    jQuery('#textRequest').attr('value',m.toJSON());
    m.post( {
        url:'intentionalError',
        /*
        onError:function(request,opt)
        {
            alert("Checking onError() handling:\n"+
                    JSON.stringify(opt,undefined,'  ')
                    );
        }
        */
        placeholder:undefined
    });
}

/**
    Uses the content of jQuery('#textRequest') to create
    a JSONMessage.Request object. It sends the message and
    prints the response to jQuery('#textResponse').
*/
function sendJSONMessage()
{
    jQuery('#inspectorTest').empty();
    var src = jQuery('#textRequest');
    var code = src.attr('value');
    if( ! code )
    {
        message("Enter a JSON message object before submitting.");
        return;
    }
    var req = null;
    try
    {
        // ensure that entered code is in JSON format...
        // The parens/braces are unfortunately necessary
        // to avoid an eval error.
        var jobj =
            //eval('['+code+'][0]')
            eval('('+code+')')
            ;
        req =
            new JSONMessage.Request(jobj) // works
            //new JSONMessage.Request(code) // also okay
            ;
        //alert(req ? req.toJSON() : "NULL JSONMessage.Request!");
    }
    catch(e)
    {
        alert("Creation of JSONMessage.Request failed while parsing JSON:\n"+e);
        return;
    }
    if( ! req )
    {
        alert("JSONMessage.Request object is null :(");
        return;
    }
    if( 0 && ('ping' === req.type()) )
    { /* just testing postOptions() */
        req.postOptions({url:'xxxYYY',method:'GET'});
    }
    var tgt = jQuery('#textResponse');
    tgt.attr('value','Waiting on response...');
    message("Posting ["+req.type()+"] request...");
    //alert( 'popt='+JSON.stringify( req.postOptions() ));
    req.post({
        onResponse:function(resp)
        {
            tgt.attr('value',resp.toJSON());
            //var iserr = resp.resultCode() ? true : false;
            message( "Result from server: resultCode="+resp.resultCode()+
                    ", resultText=["+resp.resultText()+"]."
            );
            updateResponseView();
        }
    });
}

/** Creates a JSONMessage.Request object from:

tag may either be a message type string (e.g. 'ping')
or a JSONMessage.Request instance.

payload may be null or an object containing
payload data for the message.

opt is an optional object to be assigned to request.opt(opt).

The request is toJSON()'d and jQuery('#textRequest') is
set to that value. jQuery('#textResponse') is cleared.

If the checkbox jQuery('#checkSendImmediately') is checked
then the message is also posted immediately.
*/
function createSimpleMessage(tag,payload,opt)
{
    var m = ( tag instanceof JSONMessage.Request )
        ? tag
        : (new JSONMessage.Request({type:tag}))
        ;

    if( payload ) m.payload(payload);
    if( opt ) m.set(opt);
    var src = jQuery('#textRequest');
    src.attr('value',m.toJSON());
    jQuery('#textResponse').attr('value','');
    src.fadeOut( 'fast', function(){src.fadeIn('fast');} );
    if( jQuery('#checkSendImmediately').is(':checked') )
    {
        sendJSONMessage();
    }
}

function setup_menu()
{
    /**
        Example menu structure:
    */
    function msgfunc(key,payload)
    {
        return function() { createSimpleMessage(key,payload); }
    }
    function msgitem(lbl,key,payload)
    {
        return { title:lbl, onclick:msgfunc(key,payload) };
    }

    //alert( JSON.stringify( tmerr.postOptions()  ));
    //alert(tmerr.toJSON());
   var messages = {
        opt:{
            doFlash:false
        },
        entries:[
            {title:'Server',
                entries:[
                    msgitem('Ping',
                        // These are all _basically_ equivalent:
                        'ping', {msg:'hi, world!'}
                        //(new JSONMessage.Request.Ping()).payload( {msg:'hi, world!'} )
                        //(new JSONMessage.Request({type:'ping',payload:{msg:'hi, world!'}}))
                    ),
/**
                    msgitem('$_ENV','env'),
                    msgitem('Server-side sleep 3sec', 'sleep',{seconds:3}),

                    {title:'Server sleep w/ client-side timeout',
                        onclick:function() {
                            var tmerr = new JSONMessage.Request({type:'sleep',payload:{seconds:2}});
                            jQuery('#textRequest').attr('value', tmerr.toJSON());
                            tmerr.postOptions({
                                timeout:1800,
                                onResponse:function(req,opt){
                                    alert("Sleep w/ timeout succeeded! This shouldn't have happened!");
                                },
                                onError:function(req,opt){
                                    var msg = "Sleep failed (as expected):\n"
                                        +opt.errorMessage+'\nRequest was:\n'
                                        +req.toJSON()
                                    ;
                                    jQuery('#textResponse').attr('value',msg);
                                }
                            });
                            message("Posting a sleep request where we expect a timeout: "+JSON.stringify(tmerr.postOptions()));
                            tmerr.post();
                        }
                    },
*/
                    {title:'$_SESSION',
                        entries:[
                            msgitem('Get...', new JSONMessage.Request.ServerSession(),{id:true,get:[]}),
                            msgitem('Set...',new JSONMessage.Request.ServerSession(),{id:true,set:{my:"value"}}),
                            msgitem('Clear session',new JSONMessage.Request.ServerSession(),{id:true,clearSession:true})
                        ]
                    }
                ]
            },
            {title:'Cookies',
                entries:[
                    msgitem('Set...','cookie',{set:{'my':'value'}}),
                    msgitem('Get one...','cookie',{get:['my']}),
                    msgitem('Get all...','cookie',{get:[]})
//NYI in the java back-end: msgitem('Clear all...','cookie',{clearCookies:true})
                ]
            },
/**
            {title:'Google Code',
                entries:[
                    msgitem("Fetch project's wiki list.", new JSONMessage.Request({type:'googlecode/getWikiList',
                        payload:{
                            project:"jsonmessage"
                        }})),
                    msgitem("Fetch a specific wiki page.", new JSONMessage.Request({type:'googlecode/getWikiPage',
                        payload:{
                            project:"jsonmessage",
                            page:"HomePage"
                        }}))
                ]
            },
*/
            {title:'Misc.',
                entries:[
                    //msgitem('Throw a server-side exception', new JSONMessage.Request({type:'throw',payload:{errorCode:2,message:"Throwing from the server!"}}) ),
                    //msgitem('ctor test',new JSONMessage.Request.Ping({options:{foo:"nothing"},payload:{foo:"also nothing"}})),
                    msgitem('What would Chuck Norris do?','ChuckNorris'),
                    //msgitem('/usr/games/fortune','fortune'),
                    //msgitem('Test site-local autoloads','test/localEventCheck'),
                    msgitem('No such event','noSuchEvent'),
/**
                    msgitem('Fetch an Ogre Mark III via curl proxy.', new JSONMessage.Request({type:'curl',
                                payload:{
                                    url:"http://wanderinghorse.net/gaming/ogre/byoo/ajax/json-ogre.php",
                                    json:true,
                                    parameters:{
                                        ogreID:3
                                    },
                                    failOnError: true,
                                    timeout:5000 // curl does not seem to honor this!
                                }}))
                    */
                    {title:'Toggle animations',
                        onclick:function(){
                            if( ('fx' in jQuery) )
                            {
                                jQuery.fx.off = !jQuery.fx.off;
                                message('jQuery.fx.off == '+jQuery.fx.off);
                                // Reminder: jQuery 1.2.x doesn't seem to support this.
                            }
                            else
                            {
                                message('Your jQuery appears to be missing jQuery.fx.off!');
                            }
                        }
                    }
                ]
            }
        ]
    };
    var holder = jQuery('#requestMenu');
    var bogoMenu = jQuery.createBogoMenu( messages );
    holder.empty().append( bogoMenu ).append('<br/>');
    // ^^^ BR there is a workaround for a scrolling misbehaviour when clicking the last item.
};

function changePostImpl(which)
{
    if( !( which in JSONMessage.Request.postImpl.concrete ) )
    {
        alert(which+' not found in JSONMessage.Request.postImpl.concrete!');
        return false;
    }
    JSONMessage.Request.prototype.postBackend = JSONMessage.Request.postImpl.concrete[which];
    var ta = jQuery('<textarea></textarea>').attr({rows:10,cols:80,value:''+JSONMessage.Request.prototype.postBackend});
    message("AJAX back-end changed to "+which+":<br/>",ta);
    return false/*for onclick handlers*/;
};

JSONMessage.Request.options.ajax.url = '/JSONMessage/dispatch';
jQuery(document).ready( function(JQ) {
    JQ.noConflict();
    if(1)
    { // get the header/about area out of the way when the page's title H1 is clicked.
        var head = jQuery('#headerArea');
        head.slideTime = 500;
        jQuery('h1').eq(0).toggle(
                function() { head.slideUp(head.slideTime); },
                function() { head.slideDown(head.slideTime); }
        );
    }
    if(1)
    {  // Add a visual notification when JSONMessage.Request messages are pending...
        var notif = jQuery('#ajaxNotification');
        notif.hide();
        notif.postCount = 0;
        notif.fadeSpeed = 'fast';
        notif.updateMessage = function(n)
        {
            this.postCount += n;
            this.text( this.postCount + ' pending JSONMessage.Request connection(s)...' );
            if( 1 == this.postCount ) this.fadeIn(this.fadeSpeed);
            else if( ! this.postCount ) this.fadeOut(this.fadeSpeed);
        };
        JSONMessage.Request.options.ajax.beforePost = function(req,opt)
        {
            notif.updateMessage(1);
        };
        JSONMessage.Request.options.ajax.afterPost = function(req,opt)
        {
            notif.updateMessage(-1);
        };
    }
    setup_menu();
    changePostImpl('XMLHttpRequest');
    if(1)
    {
        createSimpleMessage(new JSONMessage.Request.Ping({
            payload:{
                //id:'test',type:'foo', // demonstrate a corner-case bug in JSONMessage ctor logic
                foo:"hi, world"
            }
        }));
    }
    jQuery('textarea').attr({rows:12});
});

</script>
</head>
 <body>
  <h1>JSONMessage Demo</h1>

<div id='headerArea'>
(See also: <a href='http://code.google.com/p/jsonmessage/'>JSONMessage home page</a>)<br/>
(Click the header to hide this info text. (Assuming JavaScript is working for you.))
<br/><br/>


<div id='aboutJSONMessage'>
JSONMessage is client-server framework implementing a JSON-based messaging system.
Clients (web browsers) create messages via a high-level messaging API and send them off
to the server. The server dispatches the message to an appropriate server-side handler and responds
in a uniform JSON grammar, regardless of the request type. JSONMessage is mostly about
defining the messaging conventions, and the back-end can be implemented in arbitrary
languages/environments (e.g. PHP, Java, or Ruby).
</div><!-- #aboutJSONMessage -->

<br/>
<div>Below is a demonstration. Use the menu on the right to select message types and click the Send
button to send them. If you see no menu on the right, your browser is not capable of handling the JavaScript
used to generate the menu.
</div>

</div><!-- #headerArea -->
<hr/>

 <h2>Client Request:</h2>

<table border='1'><tr>
<td valign='top'>
<div  style='position:relative;padding:0.5em'>
    <input id='buttonRequest' type='button' value='Send Request' onclick='sendJSONMessage();return false;' style='position:absolute;top:0px;right:0px;'/>
<br/>
<textarea id='textRequest' rows='10' cols='50'>
/* Use the menu on the right to generate request events. */
</textarea>
</div>
</td>

<td width='25%' valign='top' align='left'>
<b>Event selection menu:</b><br/>
<span style='font-size:small' title='If checked, messages are sent as they are clicked. Otherwise the Send button must be used to send them.'>
<input type='checkbox' id='checkSendImmediately' checked='checked'/>Send immediately?
</span>
<br/>
<div id='requestMenu'  style='border:1px dashed black;background-color: #fafafa;height:15em;overflow:auto;padding:0px;'>
If you do not see a menu here then your browser cannot handle the truth.
</div>
</td>

</tr>
<tr>
<td valign='top' align='left'>
    <h2>Server response:</h2><span id='ajaxNotification'></span><br/>
    <textarea id='textResponse' rows='10' cols='50'></textarea>
    <br/><div id='inspectorTest'></div> 
</td><td valign='top' align='left'>
More stuff:
    <ul>
        <li><a href='#' onclick='chuckNorris();return false;'>What Would Chuck Do?</a></li>
        <li><a href='#' onclick='sendBrokenMessage();return false;'>Test onError() handling</a></li>
    </ul>


    Select <tt>JSONMessage.Request.prototype.postBackend()</tt> implementation:<br/>
    <ul>
    <li><a href='#' onclick='return changePostImpl("XMLHttpRequest");'>XMLHttpRequest</a></li>
    <li><a href='#' onclick='return changePostImpl("jQuery");'>jQuery</a></li>
    <li><a href='#' onclick='return changePostImpl("dojo");'>dojo</a></li>
    <li><a href='#' onclick='return changePostImpl("Prototype");'>Prototype</a></li>
    </ul>

    <div style='font-size:80%'>
    Only the XMLHttpRequest and jQuery implementations will work in this demonstration.
    The others will produce error messages because this page does not import those
    libraries. "Prototype" versions 1.6.x (and older) have buggy JSON handling and
    will not work reliably with JSONMessage. (This is
    <a href='https://prototype.lighthouseapp.com/projects/8886/tickets/1039-jsonstringify-incorrectly-jsonizes-empty-arrays-when-prototype-is-imported#ticket-1039-2'>supposedly fixed in Prototype 1.7</a>.)
    i have had similar problems with the Mojo toolkit, which is why no implementation
    exists for that library.
    </div>

</td>
</tr></table>

 <h2>Message log (newest items first):</h2>
 <button onclick='jQuery("#logArea").empty()'>Clear Log</button><br/>
<div id='logArea'></div>

 </body>
</html>

